<!DOCTYPE html>
<html>
  
<head><meta name="generator" content="Hexo 3.8.0">
  <meta charset="utf-8">
  <meta name="author" content="Amos Zhu">
  
  
  <title>Redis面试以及在分布式集群环境中遇到的问题 | Amos的技术博客</title>

  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

  
    <meta name="keywords" content="redis,centos7,redis,">
  

  
  <meta name="description" content="前面已经系统的梳理了Redis的各个功能和搭建的方法，下面我们就来系统的梳理下，关于Redis在系统中可能会出现的面试题">

  

  
    <script src="//cdn.jsdelivr.net/npm/leancloud-storage@3.11.1/dist/av-min.js" async></script>
  

  
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML" async></script>
  

  
    <script src="//unpkg.com/valine/dist/Valine.min.js" async></script>
  

  

  <script>
  // theme-ad's config script
  // it can be used in every script
  
  window.AD_CONFIG = {
    leancloud: {"appid":"eObxCkTJ6j4GzRxGLgiHyfJa-gzGzoHsz","appkey":"fzRMAFC6MCE11mp7mnvh4Kwe","comment":true,"count":true},
    welcome: {"enable":true,"interval":30},
    start_time: "2019-05-28",
    passwords: ["efe07af7441da2b69c4a41e42e73be4db47f66010a56900788a458354a7373ec", ],
    is_post: true,
    lock: false,
    author: "Amos Zhu",
    share: {"qq":true,"wechat":true},
    mathjax: true,
    page_type: "",
    root: "/blog/"
  };
</script>

  <script src="/blog/vendor/sha256.min.js"></script>
<script src="/blog/js/auth.js"></script>
<script src="/blog/js/index.js"></script>
<script src="/blog/vendor/qrcode.min.js"></script>

  
    <link rel="icon" href="/blog/images/favicon.ico">
    <link rel="apple-touch-icon" href="/blog/images/touch-icon.png">
  

  <link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

  <link rel="stylesheet" href="/blog/css/index.css">
<link rel="stylesheet" href="/blog/styles/components/highlight/highlight.css">

  
</head>
  <body>
    <header class="site-header">
  <div class="site-header-brand">
    
      <span class="site-header-brand-title">
        <a href="/blog/">Amos Zhu</a>
      </span>
    
    
      <span class="site-header-brand-motto"> | 当浮躁的心静不下，不妨写些东西或者看会书</span>
    
  </div>
  <div class="site-header-right">
    <nav class="site-header-navigation">
      
        <a href="/blog/" target="_self">首页</a>
      
        <a href="/blog/archives/" target="_self">归档</a>
      
        <a href="/blog/tags/" target="_self">标签</a>
      
        <a href="/blog/categories/" target="_self">分类</a>
      
        <a href="/blog/redis/" target="_self">redis</a>
      
        <a href="/blog/rabbitMQ/" target="_self">rabbitMQ</a>
      
        <a href="/blog/elasticsearch" target="_self">elasticsearch</a>
      
        <a href="/blog/stuff" target="_self">stuff</a>
      
        <a href="/blog/kafka" target="_self">kafka</a>
      
        <a href="/blog/friends/" target="_self">友链</a>
      
        <a href="/blog/about/" target="_self">关于</a>
      
    </nav>
    <div class="site-header-btn">
      
        <a href="https://gitee.com/amos_zhu" target="_blank" id="site-github">
          <i class="fa fa-github-alt"></i>
        </a>
      
      <a href="javascript:void(0);" id="site-search">
        <i class="fa fa-search"></i>
      </a>
      <a href="javascript:void(0);" id="site-nav-btn">
        <i class="fa fa-ellipsis-v"></i>
      </a>
    </div>
  </div>
</header>
<nav class="table-content" id="site-nav">
  <div class="table-content-title">
    <span>导航</span>
  </div>
  <div class="table-content-main">
    <ol class="toc">
      
        <li class="toc-item">
          <a href="/blog/" target="_self">
            首页
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/archives/" target="_self">
            归档
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/tags/" target="_self">
            标签
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/categories/" target="_self">
            分类
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/redis/" target="_self">
            redis
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/rabbitMQ/" target="_self">
            rabbitMQ
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/elasticsearch" target="_self">
            elasticsearch
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/stuff" target="_self">
            stuff
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/kafka" target="_self">
            kafka
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/friends/" target="_self">
            友链
          </a>
        </li>
      
        <li class="toc-item">
          <a href="/blog/about/" target="_self">
            关于
          </a>
        </li>
      
    </ol>
  </div>
</nav>
<div id="site-process"></div>
    <main>
      
  <div class="passage">
  <div class="passage-meta">
    <span>
      <i class="fa fa-calendar"></i>2019-06-21
    </span>
    
      <span>
        | <a href="/blog/categories/redis/"><i class="fa fa-bookmark"></i>redis</a>
      </span>
    
    
      <span>
        | <i class="fa fa-unlock-alt"></i>UNLOCK
      </span>
    
  </div>
  <h1 class="passage-title">
    Redis面试以及在分布式集群环境中遇到的问题
  </h1>
  
    <div class="passage-cover">
      <figure style="background-image:url(https://s2.ax1x.com/2019/06/21/ZS5cgs.jpg);"></figure>
    </div>
  
  <article class="passage-article">
    <ol>
<li><p>介绍Redis的集中架构模式</p>
<ul>
<li>单机型</li>
</ul>
<p>这个模型很简单，多个客户端直接连接Redis服务器<br><br>特点：内存容量有限，支持的QPS有限，无法扩容，并且没有高可用</p>
<ul>
<li>主从复制（读写分离）</li>
</ul>
<p>通过Redis的replication的功能，创建多个Master服务器从属的Slave服务器，其中Master服务器进行写操作，所有的Slave服务器进行读操作，这样只要网络正常的话，Master和Slave都具有相同的数据 <br><br>特点：支持了读QPS的扩容，但是内存容量的问题没有解决，也没有解决写QPS的高并发问题，同时也没有高可用</p>
<ul>
<li>哨兵</li>
</ul>
<p>利用Redis的sentinel分布式集群，来兼容Redis主从服务器，当Master服务器下线或者不再对外提供服务时，将Slave节点升级为Master节点，以保证Redis服务能继续对外提供服务<br><br>特点：集群监控、消息通知、故障转移和配置中心，解决了上面两个架构模式中的高可用的问题，但是主从切换需要时间，可能会造成数据部分丢失</p>
<ul>
<li>集群</li>
</ul>
<p>Redis 3.x版本之后，提供RedisCluster来创建Redis集群环境，利用RedisCluster来管理多个Redis的服务<br><br>特点：无中心架构、数据按照hashslot存储分布在多个节点，节点之间数据共享、可横向和纵向扩容，Master节点扩容，高可用并且能自动实现故障转移、节点之间通过gossip协议进行状态信息交换</p>
</li>
<li><p>Redis集群对于存储节点采用的算法是什么？能否详细介绍该算法?</p>
</li>
</ol>
<p>Redis在分布式的算法中采用了哈希槽的概念，在详细介绍哈希槽之前，我们需要先简单的了解下一致性hash算法的概念</p>
<ul>
<li>一致性hash算法</li>
</ul>
<p>一致性hash算法的总结参考了博文《<a href="https://www.cnblogs.com/lpfuture/p/5796398.html" target="_blank" rel="noopener">一致性哈希算法原理</a>》，现整理如下：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">1. 场景：如果一个集群接收多个请求，那么该怎么通过算法将每个请求分布到集群中对应的服务，并且满足高效率呢？</span><br><span class="line">2. 一致性hash算法的原理：将集群中所有的服务器的节点先计算hash值，然后将该值存放管道0~2的32次方的圆形节点中；</span><br><span class="line">发送过来的请求也计算对应的hash值，然后根据hash值到圆形节点中顺时针查找最新的服务节点，然后将请求发送过去，等待该节点服务器的响应；</span><br><span class="line">3. 如果对集群进行扩容，新增一台服务，则相当于在圆形节点上新增一个节点，</span><br><span class="line">这样当访问的请求进行hash计算后的值到圆形节点上进行匹配时，最多影响该节点相邻的节点，对其他的节点没有任何影响；</span><br><span class="line">4. 一致性hash性质：平衡性，单调性，分散性，负载和平滑性；</span><br></pre></td></tr></table></figure></p>
<ul>
<li>hashslot</li>
</ul>
<p>hashslot的算法参考了博文《<a href="https://blog.csdn.net/z15732621582/article/details/79121213" target="_blank" rel="noopener">Redis哈希槽</a>》，先整理如下：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1. RedisCluster采用无中心结构，每个节点保存数据和整个集群状态，每个节点和其他节点进行连接</span><br><span class="line">2. 所有的redis节点彼此互联（PING-PONG机制），内部使用RESP协议进行通讯，该协议具有实现简单、快速解析和可读性好的优点；</span><br><span class="line">3. 节点的fail是通过集群中超过半数的节点检测失效时才生效</span><br><span class="line">4. redis-cluster把所有的物理节点映射到0-16383个slot上，cluster负责维护node-slot-vlaue</span><br><span class="line">5. Redis集群预先分好16384个slot，当需要在Redis集群中放置一个key-value时，根据CRC16 mod 15384的值，决定将一个key放置到哪个slot中</span><br></pre></td></tr></table></figure></p>
<ol start="3">
<li>什么是缓存穿透，缓存击穿，如果避免，什么是缓存雪崩，如何避免</li>
</ol>
<ul>
<li>缓存穿透</li>
</ul>
<p>一般前端传来请求，先查询缓存，缓存中没有了在查询数据库，而缓存穿透，前端大量请求过来，查询缓存中不存在，则再查询数据库，而数据库中也不存在，则这个值无法形成缓存，那么这个请求每次都会请求数据库，这对数据库就造成了极大的压力；</p>
<p>我们可以对空也进行缓存，缓存的失效时间可以设置短点；也可以对一定不存在的key进行过滤，可以将所有可能存在的key存放在一个大的bitmap中，查询时通过bitmap进行过滤；</p>
<ul>
<li>缓存击穿</li>
</ul>
<p>是指一个Key非常热点，再不停的抗住高并发，大并发集中对这个key进行访问，如果该key失效了，则持续的高并发就会击穿缓存，直接请求数据库，严重时可能造成数据库宕机</p>
<p>其实对于热点key来说，很容易解决，只要将这个key对应的失效时间设置为永不失效即可，当然这个主要是根据业务的需求，一般大key是一段时间内的访问量是很恐怖的，但是过了这个时间段访问请求就下来了，到时再更改失效时间即可；</p>
<ul>
<li>缓存雪崩</li>
</ul>
<p>缓存雪崩是指在某个时间段内，缓存集中过期失效。这样请求都会落在数据库上，对数据库造成极大的压力，出现系统崩溃；<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">1. 我们可以将系统中不同种类的key分别设置不同的失效时间，</span><br><span class="line">比如说我们可以在原来的过期时间基础上增加一个随机值，这样的话大部分的key的过期时间就会有差别，不会集中失效&lt;br/&gt;</span><br><span class="line">2. 其次使用缓存降级，利用eehcache等本地缓存，但主要还是对源服务访问进行限流、资源隔离、降级等。</span><br><span class="line">当访问量剧增、服务出现问题仍然需要保证服务还是可用的。</span><br><span class="line">系统可以根据一些关键数据进行自动降级，也可以配置开关实现人工降级，这里会涉及到运维的配合</span><br><span class="line">3. Redis缓存的备份和预热</span><br></pre></td></tr></table></figure></p>
<ul>
<li>缓存并发</li>
</ul>
<p>是指多个redis的client同时set</p>
<p>解决的思路可以将redis的set操作放到队列中使其进行串行执行</p>
<ol start="4">
<li>Redis有哪些常见的用途</li>
</ol>
<ul>
<li>会话缓存</li>
<li>全页缓存</li>
<li>队列</li>
<li>排行榜/计数器</li>
<li>发布/订阅</li>
</ul>
<ol start="5">
<li>Redis中事务相关的命令有哪些？</li>
</ol>
<ul>
<li>MULTI</li>
<li>EXEC</li>
<li>DISCARD</li>
<li>WATCH</li>
</ul>
<ol start="6">
<li>Redis如何做内存优化</li>
</ol>
<p>在选择数据结构时，尽可能使用散列表(hashes),因为散列表使用的内存是非常小的，所以你尽可能的将你的数据模型抽象到一个散列表中</p>
<ol start="7">
<li>Redis内存回收机制以及过期策略</li>
</ol>
<p>Redis的内存监控是使用：redis-cli 中的<code>info</code>命令来查看的</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"># Memory</span><br><span class="line">used_memory:26355360#当前Redis所有key-value值及内部开销理论上要占用的内存</span><br><span class="line">used_memory_human:25.13M#上一数据的可读版本</span><br><span class="line">used_memory_rss:28127232##（Resident Set Size常驻数据集大小），可理解为OS为Redis分配的物理内存总量</span><br><span class="line">used_memory_rss_human:26.82M</span><br><span class="line">used_memory_peak:26355360#峰值内存</span><br><span class="line">used_memory_peak_human:25.13M#峰值内存可读版本</span><br><span class="line">total_system_memory:8253997056</span><br><span class="line">total_system_memory_human:7.69G</span><br><span class="line">used_memory_lua:37888#lua引擎占用内存</span><br><span class="line">used_memory_lua_human:37.00K</span><br><span class="line">maxmemory:0</span><br><span class="line">maxmemory_human:0B</span><br><span class="line">maxmemory_policy:noeviction</span><br><span class="line">mem_fragmentation_ratio:1.07#内存碎片率，used_memory_rss 和 used_memory 之间的比率</span><br><span class="line">mem_allocator:jemalloc-4.0.3#所使用的内存分配器。可以是 libc 、 jemalloc 或者 tcmalloc</span><br></pre></td></tr></table></figure>
<p>其中<code>mem_fragmentation_ratio</code>（内存碎片率）是分析Redis性能的重要数据指标</p>
<ul>
<li>大于1：OS为Redis分配的物理内存 &gt; Redis所有key-value值及内部开销应占用的内存<br>产生原因：物理内存多出的部分，Redis内移除对象的占用内存，但这部分内存由Redis自带内存分配器占用，没有向操作系统返回。这一部分就是内存碎片</li>
<li>小于1：OS为Redis分配的物理内存 &lt; Redis所有key-value值及内部开销理应占用的内存<br>产生原因：应占内存比物理内存多出的部分，是被操作系统交换到虚拟内存，说明当前Redis的内存使用已经超出物理内存</li>
</ul>
<p><strong>内存碎片率保持在1.0至1.5之间是最理想的状态</strong>。假若碎片率超过了1.5，我所知道的最有效解决手段就是重启Redis服务器，释放内存回到操作系统；反之，若碎片率为0.9，说明物理内存已不够用，应增添硬件，或设置Redis最大内存限制<code>maxmemory</code>。</p>
<p>最大内存限制<code>maxmemory</code>的设置非常重要，如果不设置<code>maxmemory</code>，Redis一直会为其分配内存，直至耗尽所有物理内存，直到操作系统进行虚拟内存交换。因此，一般情况下，作者建议还是把峰值设置设上。开启此配置，当超出限定内存情况发生，Redis会返回异常消息，操作系统不会因内存溢出而奔溃。还有一点建议是，开发者在系统设计之初，就应当制定Redis内存使用划分计划，而划分原则是，为Redis准备系统可能使用的峰值内存，而不是平均使用内存。例如系统大部分情况会以Redis作为分布式缓存写入10G数据，但大部分情况下只会跑到4G，但Redis依然推荐用户为其预留10G内存（<code>used_memory_peak</code>峰值）。</p>
<p>maxmemory的单位是bytes，默认为0，即不限制最大内存。</p>
<p>Redis的内存回收主要围绕Redis的过期策略和淘汰策略来进行的，Redis中过期策略和淘汰策略是不一样的，我们来详细了解下两者之间的区别：</p>
<ul>
<li>过期策略</li>
</ul>
<p>过期策略分为三种：定时过期、惰性过期以及定期过期</p>
<ol>
<li><p>定时过期</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">是我们通过程序向redis中新增数据的同时就已经设置好该数据的过期时间，到了定时时间就会让该数据失效，</span><br><span class="line">   这种方式对内存友好，但是对CPU的计算相对就提高了很多，这个在系统提供高并发服务的受到影响，并且影响系统的响应和吞吐量</span><br></pre></td></tr></table></figure>
</li>
<li><p>惰性过期</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">所谓的惰性过期，就是我们在访问该数据时，在判断该数据是否过期失效，如果过期则删除，</span><br><span class="line">    这种方法能大量的节省CPU的资源，但是对内存非常不友好，可能出现大量的Key没有被访问，从而也不会删除，占用大量的内存</span><br></pre></td></tr></table></figure>
</li>
<li><p>定期过期</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">每隔一段时间，就会扫描一下redis中expires字典中一定数量的key，并且清除其中已经过期的key。</span><br><span class="line">   通过定时扫描的时间间隔和每次扫描的限定损耗，在不同的情况下，使得CPU和内存资源达到相对的一种平衡</span><br></pre></td></tr></table></figure>
</li>
</ol>
<p><strong>Redis中同时用了惰性过期和定期过期</strong></p>
<ul>
<li>淘汰策略</li>
</ul>
<p>所谓淘汰策略就是当内存中内存不足，Redis中是达到redis.conf配置中的<code>maxmemory</code>的极限时，需要采用LUA淘汰算法来定期清理掉哪些数据</p>
<ol>
<li>LUA算法<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">LUA算法就是最近最少使用算法</span><br></pre></td></tr></table></figure>
</li>
</ol>
<ol start="2">
<li>缓存清理配置<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">redis.conf配置中的maxmemory配置内存达到多大时，进行内存请求，</span><br><span class="line">   在64位系统中，如果配置为0，则不限制内存的使用，</span><br><span class="line">   但是在32位系统中，默认显示内存大小是3G</span><br></pre></td></tr></table></figure>
</li>
</ol>
<ol start="3">
<li><p>Redis内存淘汰策略</p>
<p>淘汰策略是有redis.conf配置中的 maxmemory-policy来控制的，其值主要有以下几个：</p>
</li>
</ol>
<table>
<thead>
<tr>
<th>策略名称</th>
<th>策略说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>volatile-lru</td>
<td>使用LRU算法删除一个键（只对设置了生存时间的键）</td>
</tr>
<tr>
<td>allkeys-lru</td>
<td>使用LRU算法删除一个键</td>
</tr>
<tr>
<td>volatile-random</td>
<td>随机删除一个键（只对设置了生存时间的键）</td>
</tr>
<tr>
<td>allkeys-random</td>
<td>随机删除一个键</td>
</tr>
<tr>
<td>volatile-ttl</td>
<td>删除生存时间接近的一个键</td>
</tr>
<tr>
<td>noeviction</td>
<td>不删除键，只返回错误</td>
</tr>
</tbody>
</table>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">noeviction: 当内存不足时写入新的数据，写入操作会报错</span><br><span class="line">allkeys-lru: 当不存不足时写入新的数据，在键空间中，移除最近最少使用的key</span><br><span class="line">allkeys-random：当不存不足时写入新的数据，在键空间中，随机移除某个key</span><br><span class="line">volatile-lru： 当不存不足时写入新的数据，在设置过期时间的键中，移除最近最少使用的键</span><br><span class="line">volatile-random：当不存不足时写入新的数据，在设置过期时间的键中，随机移除一个键</span><br><span class="line">volatile-ttl：当内存不足时写入新的数据，在设置过期时间的键中，有更早过期时间的key优先删除</span><br></pre></td></tr></table></figure>
<ul>
<li></li>
</ul>
<ol start="8">
<li>Redis实现分布式锁</li>
</ol>
<p>在开发中锁的概念就非常重要了，我们常接触到的有线程锁、进程锁和分布式锁。</p>
<p>分布式锁就是当多个进程不在同一系统中，用分布式锁控制多个线程对资源进行访问。</p>
<p>分布式锁不仅具有锁的特征，还多了一下其他的特征</p>
<ul>
<li>互斥性：任意时刻，只有一个客户端获取锁，不能同时又多个客户端获取锁</li>
<li>安全性：锁只能被持有该锁的对象机进行解决，获取锁自动消除</li>
<li>死锁：获取锁的客户端因为某些原因宕机了而未能释放锁，其他的客户端无法获取锁</li>
<li>容错：当部分节点宕机了，客户端仍能获取锁和释放锁</li>
</ul>
<p>Redis实现分布式锁有以下两种方案：</p>
<ol>
<li>在操作某个记录时，查看是否存在对应的key记录 ，如果存在说明该记录被加锁，如果没有的话在继续添加锁，<br>并且添加锁成功之后处理自己的业务逻辑，相关的伪代码如下<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">// 加锁</span><br><span class="line"><span class="keyword">function</span> boolean lock(taskId) &#123;</span><br><span class="line">    <span class="keyword">if</span> (existsKey(taskId)) &#123;</span><br><span class="line">        <span class="built_in">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">        // 向redis中查询新的key 设置过期时间</span><br><span class="line">        setKey(taskId);</span><br><span class="line">        <span class="built_in">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 释放锁</span><br><span class="line"><span class="keyword">function</span> relaseLock(taskId) &#123;</span><br><span class="line">    <span class="keyword">if</span> (existsKey(taskId)) &#123;</span><br><span class="line">        delKey(taskId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 使用锁</span><br><span class="line">booelan lock = <span class="literal">false</span>;</span><br><span class="line">try&#123;</span><br><span class="line">    lock = lock(taskId);</span><br><span class="line">    <span class="keyword">if</span> (lock) &#123;</span><br><span class="line">        // 业务处理</span><br><span class="line">    &#125;</span><br><span class="line">&#125;catch(Exception e) &#123;</span><br><span class="line"></span><br><span class="line">&#125;finally &#123;</span><br><span class="line">    <span class="keyword">if</span> (lock) &#123;</span><br><span class="line">        relaseKey(taskId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
</ol>
<p>上面的代码会出现两个问题</p>
<ul>
<li>释放锁的代码必须保持原子性，否则在高并发的情况下会出现不可预知的问题，不能保证锁的互斥性</li>
<li>同一把锁同一个时间内可能会被其他的线程获取到，并且由其他的线程来释放锁，不能保证锁的安全性</li>
</ul>
<ol start="2">
<li>为了解决上述的问题，我们来优化代码，针对加锁和释放锁都添加原子性操作，这样单机版的redis环境就不会出现上述的问题了<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Redis中支持一个语法 `SETEX key seconds value`, 该语法可以添加一个key，并且给这个key设置过期时间和value，为了达到谁加锁，就让谁来解锁，</span><br><span class="line">    则我们可以将value设置成用户的sessionId，并且在释放锁的时候确认是否是对应用户进行操作的，</span><br><span class="line">    同时鉴于redis目前没有针对这个业务提供具有原子性的操作指令，我们可以使用lua算法来实现锁的释放，</span><br><span class="line">    完善后的伪代码跟上述的代码很像，只是修改了加锁和释放锁的操作，我们这里描述下lua算法：</span><br></pre></td></tr></table></figure>
</li>
</ol>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> readdirSync.call(<span class="string">'get'</span>, KEYS[1] == ARGV[1] <span class="keyword">then</span></span><br><span class="line">       <span class="built_in">return</span> redis.call(<span class="string">'del'</span>, KEYS[1]);</span><br><span class="line">   <span class="keyword">else</span></span><br><span class="line">       <span class="built_in">return</span> 0;</span><br><span class="line">   end</span><br></pre></td></tr></table></figure>
<p><strong>该分布式锁在单机版环境下能达到业务的需求，但是在Redis集群环境中，由于Redis的主从复制时异步的，所以上述的方案在集群环境中还是有问题的</strong></p>
<ol start="9">
<li>Redis缓存和Mysql数据库一致性的问题</li>
</ol>
<p>在高并发的业务场景中，一般都是先从缓存中读取数据，缓存中没有的再从数据库中读取，并且写入到缓存中，一般来说读的情况还好，但是数据库更新和缓存更新容易出现缓存和数据库一致性的问题</p>
<p>不管是先写数据库，再删除Redis缓存，还是先删除缓存，再写数据库，都有可能出现数据不一致的问题<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 如果先删除缓存，还没来得及写入数据库，此时又一个线程过来请求缓存了，发现缓存是空的，就会到数据库中去请求，此时缓存中的是脏数据</span><br><span class="line"></span><br><span class="line">2. 如果先写库，在删除缓存之前，写库的线程宕机了，没有删除缓存，则此时缓存中的数据就和数据库中的不一致</span><br></pre></td></tr></table></figure></p>
<p>有两种方案</p>
<ul>
<li>采用延时双删策略</li>
</ul>
<p>在写库的前后都进行redis.del(key)的操作，并且设定合理的超时时间<br>具体的操作步骤：<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1. 先删除缓存</span><br><span class="line">2. 再写数据库</span><br><span class="line">3. 休眠一段时间（具体时间由业务决定，考虑读写库的时间）</span><br><span class="line">4. 再次删除缓存</span><br><span class="line">5. 设置缓存超时</span><br></pre></td></tr></table></figure></p>
<p>该方案的弊端：采用上述的方案，最差的情况下在超时时间内数据是不一致的，而且增加了写请求的耗时，高并发的情况下对系统响应造成一定的影响。</p>
<ul>
<li>异步更新缓存（基于订阅binlog的同步机制）</li>
</ul>
<p>该方法的整体的思路如下：</p>
<p>   MySql binlog增量订阅消费+消息队列+增量数据更新到Redis</p>
<p>具体的流程如下：</p>
<p>   mysql中一旦产生了新的写入、更新、删除等操作，就可以把binlog相关的消息推送至Redis，<br>    Redis再根据binlog中的记录，对Redis进行更新。其实这种机制，很类似MySQL的主从备份机制，<br>    因为MySQL的主备也是通过binlog来实现的数据一致性。<br>    这里可以结合使用canal(阿里的一款开源框架)，通过该框架可以对MySQL的binlog进行订阅，<br>    而canal正是模仿了mysql的slave数据库的备份请求，使得Redis的数据更新达到了相同的效果。<br>    当然，这里的消息推送工具你也可以采用别的第三方：kafka、rabbitMQ等来实现推送更新Redis。</p>
<ol start="10">
<li><p>Redis为什么是单线程及高并发</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">redis是基于内存的，内存的读写速度非常快</span><br><span class="line">之所以设计为单线程，是为了减少上下文切换的时间</span><br><span class="line">使用多路复用技术，可以处理并发链接，非阻塞IO,内部实现epoll+自己实现的简单的时间框架，将所有的请求转化为事件，不在IO上花费时间</span><br></pre></td></tr></table></figure>
</li>
<li><p>Redis的注意事项</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. Master最好不要做任何持久化工作，如RDB内存快照和AOF日志文件</span><br><span class="line">2. 如果数据比较重要，某个Slave开启AOF备份数据，策略设置为每秒同步一次</span><br><span class="line">3. 为了主从复制的速度和连接的稳定性，Master和Slave最好在同一个局域网内</span><br><span class="line">4. 尽量避免在压力很大的主库上增加从库</span><br></pre></td></tr></table></figure>
</li>
</ol>
  </article>
  <aside class="table-content" id="site-toc">
  <div class="table-content-title">
    <i class="fa fa-arrow-right fa-lg" id="site-toc-hide-btn"></i>
    <span>目录</span>
  </div>
  <div class="table-content-main">
    
  </div>
</aside>
  
    <aside class="passage-copyright">
      <div>本文作者: Amos Zhu</div>
      
        <div>
          原文链接: 
          <a href target="_blank">http://amos_zhu.gitee.io/passages/Redis面试以及在分布式集群环境中遇到的问题/</a>
        </div>
      
      <div>
        版权声明: 本博客所有文章除特别声明外, 均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> 许可协议. 转载请注明出处!
      </div>
    </aside>
  
  
    <div class="passage-tags">
     
      <a href="/blog/tags/centos7/"><i class="fa fa-tags"></i>centos7</a>
     
      <a href="/blog/tags/redis/"><i class="fa fa-tags"></i>redis</a>
    
    </div>
  
</div>

    </main>
    
      
<div class="site-comment-contanier" data-plateform="leancloud">
  
    <p id="site-comment-info">
      <i class="fa fa-spinner fa-spin"></i> 评论加载中
    </p>
    <div id="site-comment"></div>
  
</div>
    
    <div class="site-footer-wrapper">
  <footer class="site-footer">
    
      
        <div class="site-footer-col">
          <h5 class="site-footer-title">博客推荐</h5>
          
            <span class="site-footer-item">
              <a href="http://youzhixueyuan.com/series/java" target="_blank">优知学院</a>
            </span>
          
            <span class="site-footer-item">
              <a href="http://ruanyifeng.com/" target="_blank">阮一峰的个人网站</a>
            </span>
          
        </div>
      
        <div class="site-footer-col">
          <h5 class="site-footer-title">系列教程</h5>
          
            <span class="site-footer-item">
              <a href="http://amos_zhu.gitee.io/blog/redis" target="_blank">Redis系列教程</a>
            </span>
          
            <span class="site-footer-item">
              <a href="http://amos_zhu.gitee.io/blog/rabbitMQ/" target="_blank">RabbitMQ教程</a>
            </span>
          
            <span class="site-footer-item">
              <a href="http://amos_zhu.gitee.io/blog/kafka/" target="_blank">Kafka架构探险</a>
            </span>
          
        </div>
      
        <div class="site-footer-col">
          <h5 class="site-footer-title">抓到我</h5>
          
            <span class="site-footer-item">
              <a href="https://gitee.com/amos_zhu" target="_blank">Gitee</a>
            </span>
          
            <span class="site-footer-item">
              <a href="https://blog.csdn.net/wdcl2468" target="_blank">CSDN</a>
            </span>
          
            <span class="site-footer-item">
              <a href="https://www.jianshu.com/u/2dbe61b1f3c3" target="_blank">简书</a>
            </span>
          
        </div>
      
    
    <div class="site-footer-info">
      <i class="fa fa-clock-o"></i> 本站已稳定运行<span id="site-time"></span>
    </div>
    
      <div class="site-footer-info">
        <i class="fa fa-paw"></i> 您是本站第 <span id="site-count"></span> 位访客
      </div>
    
    
      <div class="site-footer-info">
        <i class="fa fa-at"></i> Email: amoszhu@aliyun.com
      </div>
    
    <div class="site-footer-info">
      <i class="fa fa-copyright"></i> 
      2019 <a href="https://github.com/dongyuanxin/theme-ad/" target="_blank">Theme-AD</a>.
      Created by <a href="https://godbmw.com/" target="_blank">GodBMW</a>.
      All rights reserved.
    </div>
  </footer>
</div>
    <div id="site-layer" style="display:none;">
  <div class="site-layer-content">
    <div class="site-layer-header">
      <span class="site-layer-header-title" id="site-layer-title"></span>
      <i class="fa fa-close" id="site-layer-close"></i>
    </div>
    <div class="site-layer-body" id="site-layer-container">
      <div class="site-layer-input" id="site-layer-search" style="display: none;">
        <div class="site-layer-input-choose">
          <a href="javascript:void(0);" title="Change Search Engine">Google</a>
        </div>
        <input type="text">
        <i class="fa fa-search"></i>
      </div>
      
        <div class="site-layer-reward" id="site-layer-reward" style="display: none;">
          
            <div>
              <img src="/blog/images/wechat.png" alt="WeChat">
              
                <p>WeChat</p>
              
            </div>
          
            <div>
              <img src="/blog/images/alipay.png" alt="AliPay">
              
                <p>AliPay</p>
              
            </div>
          
        </div>
      
      <div id="site-layer-welcome" style="display:none;"></div>
    </div>
  </div>
</div>
    

<div class="bottom-bar">
  <div class="bottom-bar-left">
    <a href="/blog/passages/Redis场景应用实例/" data-enable="true">
      <i class="fa fa-arrow-left"></i>
    </a>
    <a href="/blog/passages/Redis教程（五）——RedisCluster配置/" data-enable="true">
      <i class="fa fa-arrow-right"></i>
    </a>
  </div>
  <div class="bottom-bar-right">
    <a href="javascript:void(0);" data-enable="true" id="site-toc-show-btn">
      <i class="fa fa-bars"></i>
    </a>
    
      <a href="#site-comment" data-enable="true">
        <i class="fa fa-commenting"></i>
      </a>
    
    <a href="javascript:void(0);" id="site-toggle-share-btn">
      <i class="fa fa-share-alt"></i>
    </a>
    
      <a href="javascript:void(0);" id="site-reward">
        <i class="fa fa-thumbs-up"></i>
      </a>
    
    <a href="javascript:void(0);" id="back-top-btn">
      <i class="fa fa-chevron-up"></i>
    </a>
  </div>
</div>
    <div id="share-btn">
  
  
  
  
    <a id="share-btn-qq" href="javascript:void(0);" target="_blank">
      <i class="fa fa-qq"></i>
    </a>
  
  
    <a id="share-btn-wechat" href="javascript:void(0);" target="_blank">
      <i class="fa fa-wechat"></i>
    </a>
  
</div>
    


  <script async>
  (function(){
      var bp = document.createElement('script');
      var curProtocol = window.location.protocol.split(':')[0];
      if (curProtocol === 'https') {
          bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
      }
      else {
          bp.src = 'http://push.zhanzhang.baidu.com/push.js';
      }
      var s = document.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(bp, s);
  })();
  </script>



  <script async>
    (function(){
    var src = (document.location.protocol == "http:") ? "http://js.passport.qihucdn.com/11.0.1.js?":"https://jspassport.ssl.qhimg.com/11.0.1.js?";
    document.write('<script src="' + src + '" id="sozz"><\/script>');
    })();
  </script>


    
  </body>
</html>